package com.meshnetics.gb.type.impl.additional;

import com.meshnetics.gb.type.impl.zcl.AbstractDataValue;
import com.meshnetics.gb.util.Conversions;
import com.meshnetics.gb.util.Utils;
import com.meshnetics.gb.util.checkers.RangeCheckers;
import com.meshnetics.gb.util.formatters.NumberFormatter;
import com.meshnetics.gb.util.formatters.NumberFormatters;

import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Base type for enumerations. It guarantees that codes and names are unique
 * within instances of same Enumeration class. For using it is need just to extend
 * this class and create several instances of enum. They can be obtained through static
 * methods valueOf(Class, int), valueOf(Class, String) or valueOf(Class, ByteBuffer).
 * For example we have a class: <code><p/>
 * public final class MyEnum extends Enumeration { <br/>
 * &nbsp&nbsp public static final MyEnum ONE = new MyEnum("One", 1); <br/>
 * &nbsp&nbsp public static final MyEnum TWO = new MyEnum("Two", 2); <br/>
 * <br/>
 * &nbsp&nbsp private MyEnum(final String name, final int code) { <br/>
 * &nbsp&nbsp&nbsp&nbsp&nbsp super(name, code); <br/>
 * &nbsp&nbsp } <br/>
 * <br/>
 * &nbsp&nbsp protected String getEnumName() { <br/>
 * &nbsp&nbsp&nbsp&nbsp&nbsp return "my enums"; <br/>
 * &nbsp&nbsp } <br/>
 * <br/>
 * &nbsp&nbsp public MyEnum valueOf(final int code) { <br/>
 * &nbsp&nbsp&nbsp&nbsp&nbsp return (MyEnum) valueOf(MyEnum.class, code); <br/>
 * &nbsp&nbsp } <br/>
 * <br/>
 * &nbsp&nbsp public MyEnum valueOf(final ByteBuffer buffer) { <br/>
 * &nbsp&nbsp&nbsp&nbsp&nbsp return (MyEnum) valueOf(MyEnum.class, buffer); <br/>
 * &nbsp&nbsp } <br/>
 * <br/>
 * &nbsp&nbsp public MyEnum valueOf(final String name) { <br/>
 * &nbsp&nbsp&nbsp&nbsp&nbsp return (MyEnum) valueOf(MyEnum.class, name); <br/>
 * &nbsp&nbsp } <br/>
 * } <br/>
 * </code><p/><b>Note 1:</b> code is int used as u-byte, so it cannot be less than zero
 * and greater than 255.<br/><b>Note 2:</b> if subclass during calling getEnumName
 * method returns null uncommented exception will be thrown within enumeration
 * constructor.
 *
 * @author Yury Aserchev (yaserchev@meshnetics.com)
 *         Date: 27.03.2008 17:29:33
 */
public abstract class Enumeration extends AbstractDataValue {
    /** NumberFormatter used for enum code formatting */
    public static final NumberFormatter FORMATTER = NumberFormatters.UBYTE;
    /** Map for storing EnumStorer instances appropriate to Enumeration concrete class [Class -> EnumStorer] */
    private static final Map BY_CLASS = new HashMap(); // <Class, EnumStorer>
    /** Enumeration instance name */
    private final String name;
    /** Enumeration instance code */
    private final int code;
    /** Enumeration cached hash */
    private final int hash;
    /** Enumeration cached String representation */
    private final String toString;
    /** Enumeration cached simplified String representation. */
    private final String simpleString;

    /**
     * Creates new instance of enum.
     * @param type enumeration type.
     * @param name enum instance name.
     * @param code enum insance code.  <b>Note: code is int used as u-byte.</b>
     * @throws NullPointerException if name is null.
     * @throws IllegalArgumentException if code if out of u-byte range.
     * @throws IllegalStateException if code or name is not unique within same Enumeration class.
     */
    protected Enumeration(final TypeEnumeration type, final String name, final int code) {
        super(type);
        if (name == null) {
            throw new NullPointerException("parameter name is null");
        }
        RangeCheckers.UBYTE.checkRange(code);
        final Class ownerClass = getClass();
        final String ownerClassName = ownerClass.getName();
        EnumStorer es = (EnumStorer) BY_CLASS.get(ownerClass);
        if (es == null) {
            es = new EnumStorer();
            BY_CLASS.put(ownerClass, es);
        }
        this.code = code;
        this.name = name;
        this.hash = name.hashCode() * 29 + code;
        es.addEnum(this, ownerClassName);
        toString = new StringBuffer(getEnumName()).append("[name: '").append(name).append("', code: ").
                append(FORMATTER.formatNumber(code)).append("]").toString();
        simpleString = new StringBuffer(getEnumName()).append(": ").append(name).toString();
    }

    /**
     * Returns name of enum instance.
     * @return name.
     */
    public final String getName() {
        return name;
    }

    /**
     * Returns code of enum instance.  <b>Note: code is int used as u-byte.</b>
     * @return code.
     */
    public final int getCode() {
        return code;
    }

    public int getDataSize() {
        return 1;
    }

    public byte[] bytesValue() {
        return new byte[]{(byte) code};
    }

    public Object toObject() {
        return new Integer(code);
    }

    public void serialize(final ByteBuffer buffer) {
        Utils.checkBuffer(buffer, 1);
        buffer.put((byte) code);
    }

    public boolean isInvalid() {
        return false;
    }

    /**
     * Returns logical name of enumeration used in string representation of this
     * enumeration. For example for ConnectionType enum it can return "connection type"
     * <br/><b>Note:</b> if subclass during calling getEnumName method returns null
     * uncommented exception will be thrown within enumeration constructor.
     * @return enumeration name.
     */
    protected abstract String getEnumName();

    /**
     * Returns String representation of an enum instance, i.e. Enum name, enum
     * instance name, enum instance code.
     * @return string representation.
     */
    public final String toString() {
        return toString;
    }

    /**
     * Returns simplified String representation of an enum instance, i.e. Enum name
     * and enum instance name. For example: <br/>My enum: One.
     * @return string representation.
     */
    public final String getSimpleString() {
        return simpleString;
    }

    public int hashCode() {
        return hash;
    }

    /**
     * Returns EnumStorer appropriate to the passed class.
     * @param ownerClass source class.
     * @return target EnumStorer.
     * @throws NullPointerException if passed class is null.
     * @throws IllegalArgumentException if no EnumStorer found for passed class.
     */
    static EnumStorer getESByClass(final Class ownerClass) {
        if (ownerClass == null) {
            throw new NullPointerException("parameter ownerClass is null");
        }
        final EnumStorer es = (EnumStorer) BY_CLASS.get(ownerClass);
        if (es == null) {
            throw new IllegalArgumentException("There is no registered enumeration instances for class " +
                    ownerClass.getName());
        }
        return es;
    }

    /**
     * Returns Enumeration instance appropriate to passed class and code.
     * @param ownerClass owner class.
     * @param code enumeration code.
     * @return target Enumeration instance.
     * @throws IllegalArgumentException if passed code is out of u-byte range, if there
     * are no Enumeration instances registered for this class, if there is no enumeration
     * instance for passed class and code.
     * @throws NullPointerException if passed class is null.
     */
    protected static Enumeration valueOf(final Class ownerClass, final int code) {
        RangeCheckers.UBYTE.checkRange(code);
        final EnumStorer es = getESByClass(ownerClass);
        final Enumeration enumeration = es.getByCode(code);
        if (enumeration == null) {
            final StringBuffer sb = new StringBuffer("there is no enumeration instance for code ").
                    append(FORMATTER.formatNumber(code)).append(" for class ").append(ownerClass.getName());
            throw new IllegalArgumentException(sb.toString());
        }
        return enumeration;
    }

    /**
     * Returns Enumeration instance appropriate to passed class and code, restored from
     * passed byte buffer. Checks if buffer has enough data before restoring. As code is
     * u-byte it restores one byte only.
     * @param ownerClass owner class.
     * @param buffer source byte buffer.
     * @return target Enumeration instance.
     * @throws IllegalArgumentException if passed buffer has not enough data, if there
     * are no Enumeration instances registered for this class, if there is no enumeration
     * instance for passed class and restored code.
     * @throws NullPointerException if passed class is null; if passed buffer is null.
     */
    protected static Enumeration valueOf(final Class ownerClass, final ByteBuffer buffer) {
        Utils.checkBuffer(buffer, 1);
        return valueOf(ownerClass, Conversions.expandByteToInt(buffer.get()));
    }

    /**
     * Returns Enumeration instance appropriate to passed class and name.
     * @param ownerClass owner class.
     * @param name enumeration name.
     * @return target Enumeration instance.
     * @throws IllegalArgumentException if there are no Enumeration instances registered
     * for this class, if there is no enumeration instance for passed class and name.
     * @throws NullPointerException if passed class is null.
     */
    protected static Enumeration valueOf(final Class ownerClass, final String name) {
        final EnumStorer es = getESByClass(ownerClass);
        final Enumeration enumeration = es.getByName(name);
        if (enumeration == null) {
            final StringBuffer sb = new StringBuffer("there is no enumeration instance for name '").
                    append(name).append("' for class ").append(ownerClass.getName());
            throw new IllegalArgumentException(sb.toString());
        }
        return enumeration;
    }

    /**
     * Test purpose method - returns unmodifiable copy of internal map EnumStorer by Class, i.e. key is Class.
     * @return unmodifiable map [Class -> EnumStorer].
     */
    protected static Map getMapESByClass() {
        return Collections.unmodifiableMap(BY_CLASS);
    }

    /**
     * Test purpose method - returns unmodifiable copy of internal map from EnumStorer - Enumeration by
     * code, i.e. key is Integer.
     * @param aClass owmer class which EnumStorer is find for.
     * @return unmodifiable map [Integer -> Enumeration].
     */
    protected static Map getAllEnumsByCode(final Class aClass) {
        return ((EnumStorer) BY_CLASS.get(aClass)).getMapEnumByCode();
    }

    /**
     * Test purpose method - returns unmodifiable copy of internal map from EnumStorer - Enumeration by
     * name, i.e. key is String.
     * @param aClass owmer class which EnumStorer is find for.
     * @return unmodifiable map [String -> Enumeration].
     */
    protected static Map getAllEnumsByName(final Class aClass) {
        return ((EnumStorer) BY_CLASS.get(aClass)).getMapEnumByName();
    }

    /**
     * Storer of enumeration instances appropriate for each class, i.e. one EnumStorer
     * stores all instances of Enumerations of concrete class.
     */
    static final class EnumStorer {
        /** Map for storing Enumeration instances appropriate to code [Integer -> EnumStorer] */
        private final Map byCode = new HashMap(); // <Integer, Enumeration>
        /** Map for storing Enumeration instances appropriate to name [String -> EnumStorer] */
        private final Map byName = new HashMap(); // <String, Enumeration>

        /**
         * Check if name of creating enum is unique within this storer
         * @param name enum name
         * @param code enum code
         * @param ownerClassName owner class name
         * @throws IllegalStateException if enum with same name is already registered in storer
         */
        private void checkName(final String name, final int code, final String ownerClassName) {
            final Enumeration existByName = (Enumeration) byName.get(name);
            if (existByName != null) {
                final StringBuffer sb = new StringBuffer("Name '").append(name).
                        append("' is already registered with code ").
                        append(FORMATTER.formatNumber(existByName.getCode())).append(", new code ").
                        append(FORMATTER.formatNumber(code)).append(", owner class ").append(ownerClassName);
                throw new IllegalStateException(sb.toString());
            }
        }

        /**
         * Check if code of creating enum is unique within this storer
         * @param name enum name
         * @param code enum code
         * @param ownerClassName owner class name
         * @throws IllegalStateException if enum with same code is already registered in storer
         */
        private void checkCode(final int code, final String name, final String ownerClassName) {
            final Enumeration existByCode = (Enumeration) byCode.get(new Integer(code));
            if (existByCode != null) {
                final StringBuffer sb = new StringBuffer("Code ").append(FORMATTER.formatNumber(code)).
                        append(" is already registered with name '").append(existByCode.getName()).
                        append("', new name '").append(name).append("', owner class ").append(ownerClassName);
                throw new IllegalStateException(sb.toString());
            }
        }

        /**
         * Adds enum to the storer
         * @param e enum
         * @param ownerClassName owner class name
         * @throws IllegalStateException if name or code is not unique within the storer
         */
        public void addEnum(final Enumeration e, final String ownerClassName) {
            checkName(e.getName(), e.getCode(), ownerClassName);
            checkCode(e.getCode(), e.getName(), ownerClassName);
            byCode.put(new Integer(e.getCode()), e);
            byName.put(e.getName(), e);
        }

        /**
         * Returns enum instance from the storer by code
         * @param code enum code
         * @return instance or null if not found
         */
        public Enumeration getByCode(final int code) {
            return (Enumeration) byCode.get(new Integer(code));
        }

        /**
         * Returns enum instance from the storer by name
         * @param name enum name
         * @return instance or null if not found
         */
        public Enumeration getByName(final String name) {
            return (Enumeration) byName.get(name);
        }

        /**
         * Test purpose method - returns unmodifiable copy of internal map Enumeration by name, i.e. key is String.
         * @return unmodifiable map [String -> EnumStorer]
         */
        Map getMapEnumByName() {
            return Collections.unmodifiableMap(byName);
        }

        /**
         * Test purpose method - returns unmodifiable copy of internal map Enumeration by code, i.e. key is Integer.
         * @return unmodifiable map [Integer -> EnumStorer]
         */
        Map getMapEnumByCode() {
            return Collections.unmodifiableMap(byCode);
        }
    }
}
